package org.cogaen.sound.xp;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.util.LinkedList;
import java.util.Queue;

import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.SourceDataLine;

import org.cogaen.core.Core;
import org.cogaen.core.Service;
import org.cogaen.logging.LoggingService;
import org.cogaen.resource.ResourceManager;

public class SoundManager implements Service {

	public static final String NAME = "cogaen.soundmanager";
	public static final String LOGGING_SOURCE = "SoundManager";
	
	private AudioFormat playbackFormat = new AudioFormat(44100, 16, 2, true, false);
	private Queue<SoundPlayer> players = new LinkedList<SoundPlayer>();
	private ResourceManager resMngr;
	private LoggingService logger;
	private Core core;
	
	@Override
	public void initialize(Core core) {
		this.core = core;
		this.logger = LoggingService.getInstance(core);
		this.resMngr = ResourceManager.getInstance(core);
		
		SoundPlayer[] preload = new SoundPlayer[3];
		for (int i = 0; i < 3; ++i) {
			preload[i] = getPlayer();
		}
		for (int i = 0; i < 3; ++i) {
			addToPool(preload[i]);
		}
	}

	@Override
	public String getName() {
		return NAME;
	}
	
	/**
	 * Returns the installed instance of this service.
	 * 
	 * <p>This is a convenience method. A call to this method is identical
	 * to the following code:
	 * <pre>
	 * (LoggingService) core.getService( LoggingService.NAME );
	 * </pre>
	 * </p>
	 * 
	 * @param core reference to a instance of core.
	 * @return installed service instance.
	 */
	public static SoundManager getInstance(Core core) {
		return (SoundManager) core.getService(NAME);
	}

	public Sound loadSound(String resName) {
		return (Sound) this.resMngr.getResource(resName);		
	}
		
	public void playSound(Sound sound) {
		if (sound == null) {
			return;
		}
		
		SoundPlayer player = getPlayer();
		player.play(sound);
	}

	public AudioFormat getPlaybackFormat() {
		return this.playbackFormat;
	}
	
	private synchronized SoundPlayer getPlayer() {
		if (this.players.isEmpty()) {
			try {
				this.logger.logDebug(LOGGING_SOURCE, "creating new sound player");
				SoundPlayer player = new SoundPlayer(getPlaybackFormat());
				return player;
			} catch (LineUnavailableException e) {
				this.logger.logWarning(LOGGING_SOURCE, "unable to create new sound player");
				return null;
			}
		}
		this.logger.logDebug(LOGGING_SOURCE, "reusing soundplayer, " + (this.players.size()  - 1) + " players left");
		return this.players.remove();
	}
	
	private synchronized void addToPool(SoundPlayer player) {
		this.players.add(player);
		this.logger.logDebug(LOGGING_SOURCE, "soundplayer added to pool, now holding " + this.players.size() + " players");
	}
	
	private class SoundPlayer extends Thread {

		private static final double SECONDS_TO_BUFFER = 0.1;
		private SourceDataLine line;
		private byte[] buffer;
		private boolean play;
		private boolean shutdown;
		private Sound sound;
		
		public SoundPlayer(AudioFormat format) throws LineUnavailableException {
			DataLine.Info lineInfo = new DataLine.Info(SourceDataLine.class, format);
			this.line = (SourceDataLine) AudioSystem.getLine(lineInfo);
			
			int bufferSize = (int) (format.getFrameSize() * Math.round(format.getSampleRate() * SECONDS_TO_BUFFER));
			this.line.open(format, bufferSize);
			this.line.start();
			this.buffer = new byte[bufferSize];
			this.shutdown = false;
			this.play = false;
			this.sound = null;
			start();
		}
				
		public synchronized void shutdown() {
			this.line.drain();
			this.line.close();
			this.shutdown = true;
			notify();
		}
		
		public synchronized void play(Sound sound) {
			if (this.play || this.shutdown) {
				throw new IllegalStateException();
			}
			this.sound = sound;
			this.play = true;
			notify();
		}
				
		@Override
		public synchronized void run() {
			while (!this.shutdown) {
				while (!this.shutdown && !this.play) {
					try {
						wait();
					} catch (InterruptedException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
				}
				if (!this.shutdown) {
					doPlaySound();
					this.sound = null;
					this.play = false;
					SoundManager.this.addToPool(this);
				}
			}
		}

		private void doPlaySound() {
			ByteArrayInputStream is = new ByteArrayInputStream(this.sound.getSamples());
			
			int numBytesRead = 0;
			while (numBytesRead != -1) {
				numBytesRead = is.read(buffer, 0, buffer.length);
				if (numBytesRead != -1) {
					this.line.write(buffer, 0, numBytesRead);
				}
			}
		}
	}
}
